﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using STSdb4.Database;
using fastJSON;
using System.IO;

namespace Com.SuperCache.Engine
{
    public class STSdbCache : BaseCache
    {
        private const string UsageStat = "SuperCacheUsageStat";
        private string dataPath;
        private static object syncRoot = new object();
        private RecycleAlgorithms recycleAlgorithm;
        private int maxCount;
        private int threshold;
        private RecycleModes recycleMode;

        public STSdbCache(string DataPath, RecycleAlgorithms RecycleAlgorithm, int MaxCount, int Threshold, RecycleModes RecycleMode)
        {
            dataPath = DataPath;
            if (!dataPath.EndsWith(Path.DirectorySeparatorChar.ToString()))
                dataPath += Path.DirectorySeparatorChar;

            recycleAlgorithm = RecycleAlgorithm;
            maxCount = MaxCount;
            threshold = Threshold;
            recycleMode = RecycleMode;
        }

        public override void Add<K>(string Category, K Key, object Data)
        {
            Add(Category, Key, Data, null);
        }

        private IStorageEngine Engine
        {
            get
            {
                return STSdb.FromFile(GetFile(false), GetFile(true));
            }
        }

        private string GetExpirationTable(string Category)
        {
            return KeyExpiration + "_" + Category;
        }

        public override void Add<K, V>(string Category, IEnumerable<KeyValuePair<K, V>> Items, DateTime? ExpirationDate)
        {
            long count = 0;

            lock (syncRoot)
            {
                var engine = Engine;
                var table = engine.OpenXIndex<K, string>(Category);
                var expiration = engine.OpenXIndex<K, DateTime>(GetExpirationTable(Category));

                //track recycle
                IIndex<K, int> usage = null;
                if (recycleAlgorithm != RecycleAlgorithms.None)
                    usage = engine.OpenXIndex<K, int>(UsageStat);

                Items.ForEach(i =>
                {
                    var key = i.Key;
                    var data = i.Value;
                    //will only serialize object other than string
                    var result = typeof(V) == typeof(string) ? data as string : JSON.Instance.ToJSON(data);
                    table[key] = result;
                    table.Flush();

                    //specify expiration
                    //default 30 mins to expire from now
                    var expirationDate = ExpirationDate == null || ExpirationDate <= DateTime.Now ? DateTime.Now.AddMinutes(30) : (DateTime)ExpirationDate;
                    expiration[key] = expirationDate;
                    expiration.Flush();

                    //track recycle
                    if (usage != null)
                    {
                        usage[key] = 0;
                        if (recycleMode == RecycleModes.Active)
                            Recycle<K>(Category, usage.Count());
                    }
                });

                if (usage != null)
                    count = usage.Count();

                engine.Commit();

                //only dispose disk-based engine
                engine.Dispose();
            }

            if (recycleMode == RecycleModes.Passive)
                Recycle<K>(Category, count);
        }

        public override void Add<K>(string Category, K Key, object Data, DateTime? ExpirationDate)
        {
            Add<K, object>(Category, new List<KeyValuePair<K, object>> { new KeyValuePair<K, object>(Key, Data) }, ExpirationDate);
        }

        private string GetFile(bool IsData)
        {
            if (!Directory.Exists(dataPath))
                Directory.CreateDirectory(dataPath);
            return dataPath + "SuperCache." + (IsData ? "dat" : "sys");
        }

        public override List<KeyValuePair<K, V>> Get<K, V>(string Category, IEnumerable<K> Keys)
        {
            var result = new List<KeyValuePair<K, V>>();
            lock (syncRoot)
            {
                var engine = Engine;
                var table = engine.OpenXIndex<K, string>(Category);
                var expiration = engine.OpenXIndex<K, DateTime>(GetExpirationTable(Category));
                var isCommitRequired = false;

                //track recycle
                IIndex<K, int> usage = null;
                if (recycleAlgorithm != RecycleAlgorithms.None)
                    usage = engine.OpenXIndex<K, int>(UsageStat);

                Keys.ForEach(key =>
                {
                    string buffer;
                    V value;
                    if (table.TryGet(key, out buffer))
                    {
                        //will only deserialize object other than string
                        value = typeof(V) == typeof(string) ? (V)(object)buffer : JSON.Instance.ToObject<V>(buffer);
                        bool needUpdate = true;
                        DateTime expirationDate;
                        //get expiration date
                        if (expiration.TryGet(key, out expirationDate))
                        {
                            //expired
                            if (expirationDate < DateTime.Now)
                            {
                                value = default(V);
                                table.Delete(key);
                                table.Flush();
                                expiration.Delete(key);
                                expiration.Flush();
                                isCommitRequired = true;
                                needUpdate = false;
                            }
                        }

                        //track recycle
                        if (usage != null && needUpdate)
                        {
                            usage[key]++;
                            isCommitRequired = true;
                        }
                    }
                    else
                        value = default(V);

                    result.Add(new KeyValuePair<K, V>(key, value));
                });

                //only need to commit write actions
                if (isCommitRequired)
                    engine.Commit();

                //only dispose disk-based engine
                engine.Dispose();
            }
            return result;
        }

        public override V Get<K, V>(string Category, K Key)
        {
            var buffer = Get<K, V>(Category, new K[] { Key });
            var result = buffer.FirstOrDefault();
            return result.Value;
        }

        public override void Recycle<K>(string Category, long Count)
        {
            if (Count < maxCount)
                return;

            switch (recycleAlgorithm)
            {
                case RecycleAlgorithms.MRU:
                    lock (syncRoot)
                    {
                        var engine = Engine;
                        var table = engine.OpenXIndex<K, string>(Category);
                        var expiration = engine.OpenXIndex<K, DateTime>(GetExpirationTable(Category));
                        var usage = engine.OpenXIndex<K, int>(UsageStat);
                        //find out expired items
                        var expired = expiration.Where(e => e.Value < DateTime.Now);
                        expired.ForEach(e =>
                        {
                            table.Delete(e.Key);
                            expiration.Delete(e.Key);
                            usage.Delete(e.Key);
                        });

                        //find out least used items
                        var leastUsed = usage.OrderByDescending(s => s.Value).Skip(maxCount - threshold);
                        leastUsed.ForEach(u =>
                        {
                            table.Delete(u.Key);
                            expiration.Delete(u.Key);
                            usage.Delete(u.Key);
                        });

                        table.Flush();
                        expiration.Flush();
                        usage.Flush();
                        engine.Commit();

                        engine.Dispose();
                    }
                    break;
                default:
                    break;
            }
        }
    }
}